package cn.com.w.simple.demo.config;

import cn.com.w.simple.demo.filter.CustomEntryPointFilter;
import cn.com.w.simple.demo.filter.CustomLoginFilter;
import cn.com.w.simple.demo.service.AbstractLoginHandler;
import com.alibaba.fastjson.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.cors.CorsUtils;

import javax.annotation.Resource;

/**
 * spring security 配置类
 * <p>
 * WebSecurityConfigurerAdapter = 核心类
 * adapter：适配器
 *
 *
 * 对于单体系统，使用Spring Security是非常简单的，因为Spring Security是一个配置驱动的框架，通过配置就可以实现授权和认证操作。
 * Spring Security自带了一个WebSecurityConfigurerAdapter类，这个类是开发人员操作Spring Security最主要的入口。
 *
 *
 *
 * 抽象类WebSecurityConfigurerAdapter中默认的configure方法:
 * protected void configure(HttpSecurity http) throws Exception {
 *         this.logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");
 *         // 对所有访问HTTP端点 的请求进行限制
 *         http.authorizeRequests((requests) -> {
 *             // requests.anyRequest().authenticated()表示所有的请求都要认证，
 *             ((ExpressionUrlAuthorizationConfigurer.AuthorizedUrl)requests.anyRequest()).authenticated();
 *         });
 *         // 表单登录认证
 *         http.formLogin();
 *         // 执行HTTP基础认证
 *         http.httpBasic();
 *     }
 * 也就是说在系统自带的抽象类中WebSecurityConfigurerAdapter已经做了一层控制，只需要引入Spring Security就可以做系统拦截。
 *
 *
 * 该类实现了WebSecurityConfigurer接口，重写了configure方法，在该方法中通过http.authorizeRequests添加请求限制，即哪些请求需要认证，
 * requests.anyRequest().authenticated()表示所有的请求都要认证，
 *
 * 认证方式有两种:
 * 一种是通过http.formLogin() 执行表单登录认证，
 * 一种是通过http.httpBasic()执行HTTP基础认证
 * 如果使用了表单认证，在浏览器访问时会出现登录界面，如果想要做一些定制化的内容，例如自定义登录界面、登录表单提交时处理地址、定制化登录界面和操作入口等，都是可以做定制化设置的。
 * 例如使用loginPage设置登录界面，使用loginProcessingUrl设置认证方法地址，使用defaultSuccessUrl设置登录成功后跳转的地址。
 *
 * 现在都是前后端分离项目，因此一般不会使用formLogin来做这些事情。
 * public abstract class WebSecurityConfigurerAdapter implements WebSecurityConfigurer<WebSecurity> {
 *     @Override
 *     protected void configure(HttpSecurity http) throws Exception {
 *         http.formLogin()
 *                 // 自定义登录页面
 *                 .loginPage("/login.html")
 *                 // 登录表单提交时的处理地址
 *                 .loginProcessingUrl("/action")
 *                 // 登录认证成功后的跳转页面
 *                 .defaultSuccessUrl("/index");
 *     }
 * }
 *
 */
@Configuration
@EnableWebSecurity(debug = true)// 开启Spring Security的debug调试模式,可以在控制台看到过滤器的执行顺序
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private UserDetailsService userService;


    @Resource
    @Qualifier(value = "namePassLoginImpl")
    private AbstractLoginHandler namePassLoginImpl;


    /**
     * 重写认证方法
     *
     * 在Spring Security中：
     * UserDetails用来描述Spring Security中的用户
     * UserDetailsService定义了对UserDetails的查询操作
     * UserDetailsManager扩展了UserDetailsService，定义了对UserDetails的新增和修改操作。
     *
     * @param builder
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder builder) throws Exception {
        UserDetails details = userService.loadUserByUsername("lisi");
        System.out.println("用户:" + JSONObject.toJSONString(details));

        builder
                // 指定认证对象的来源
                .userDetailsService(userService)
                // 密码加密方式
                .passwordEncoder(passwordEncoder());;
    }


    /**
     * 重写授权方法
     *
     * 虽然此配置类让spring security不再提供默认登录页，但是可以访问所有资源。
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        // 关闭csrf，如果不关闭会影响前端请求接口
        http.csrf().disable();
        // 关闭frameOptions,防止iframe内容无法显示
        http.headers().frameOptions().disable();
        // 开启跨域以便前端调用接口
        http.cors();


        // 这是配置的关键，决定哪些接口开启防护，哪些接口绕过防护
        http.authorizeRequests((requests) -> {
            // 对于所有请求都需要执行认证
            ((ExpressionUrlAuthorizationConfigurer.AuthorizedUrl)requests
                    // 注意这里，是允许前端跨域联调的一个必要配置
                    .requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
                    // 指定某些接口不需要通过验证即可访问。登陆、注册接口肯定是不需要认证的
                    .antMatchers("/login", "/register").permitAll()
                    // 这里意思是其它所有接口需要认证才能访问
                    .anyRequest()).authenticated();
        }).addFilter(new CustomLoginFilter(authenticationManager(), namePassLoginImpl));

        // 指定认证错误处理器
        //http.exceptionHandling().authenticationEntryPoint(new CustomEntryPointFilter());

        // HTTP基础认证
        // 在请求头中会发现 Authorization : Basic dXNlcjo3Y2FiOWEwYy0yZThkLTQ2YzEtOWNlNS01ZjZjMWRhMTM3ZGY=
        http.httpBasic();

        //http.formLogin().loginProcessingUrl("/login").and().logout().logoutUrl("/logout");

    }


    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        /**
         * 往数据库中添加用户数据时就要将密码进行加密，否则后续进行密码校验时从数据库拿出来的还是明文密码，是无法通过校验的。比如我们有一个用户注册的接口：
         * @Autowired
         * private PasswordEncoder passwordEncoder;
         *
         * @PostMapping("/register")
         * public String register(@RequestBody UserParam param) {
         *     UserEntity user = new UserEntity();
         *     // 调用加密器将前端传递过来的密码进行加密
         *     user.setUsername(param.getUsername()).setPassword(passwordEncoder.encode(param.getPassword()));
         *     // 将用户实体对象添加到数据库
         *     userService.save(user);
         *     return "注册成功";
         * }
         */
        // 使用bcrypt加密算法，安全性比较高
        return new BCryptPasswordEncoder();
    }


    @Bean
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

}
